https 以及https 在Android 中的使用

http 是明文传输的,被攻击者截获以后会出现一些问题。客户端可能访问的是一个攻击者的服务端(被劫持),导致客户数据泄漏。服务端接收的是一个攻击者的客户端,可能导致带宽损失、数据被修改等等。对于安全级别比较高的应用仅仅的http 已经不在适用了。
解决办法是加密:通信加密(使用ssl 或者tls),内容加密(http 报文加密)。
与ssl 组合的http 称为https。本文介绍一些https 的知识(https 如何做到安全的),以及https 在Android 端的使用。

加密技术使https 安全

介绍两种常见的密码学中的加密技术。

对称加密

加解密使用同一个密钥。优点:速度比较快(相对非对称加密)。缺点:没有办法安全的传输密钥。

非对称加密

使用公钥加密,然后使用私钥解密。优点:公钥是直接公开,而私钥不需要传输,安全。缺点:无法验证公钥的正确性。

证书认证机构(CA)

为了解决非对称加密无法验证公钥的正确性的问题,会使用第三方权威机构来认证公钥的正确性。流程如下:

  1. 服务器运营人员用‘服务器公钥’向CA请求证书。
  2. CA用私钥对‘服务器公钥’加密得到‘证书’,然后给服务器运营人员颁布‘证书’。
  3. 客户端请求服务器时会使用CA公钥对‘证书’校验(这里可以验证’证书’的前提是CA公钥是正确的,CA公钥一般随着浏览器的发布植入到浏览器,安全)获取‘服务器公钥’。
  4. 后续使用‘服务器公钥’对数据加密,服务器使用‘服务器私钥’进行解密。

https 使用非对称加密来传递通信使用的密钥,使用对称加密来通信。流程如下:

  1. 客户端使用‘服务器公钥’(通过CA 认证)加密‘对称加密的密钥’给服务器。
  2. 服务器使用‘服务器私钥’解密得到‘对称加密的密钥’
  3. 后续使用‘对称加密的密钥’来通信

总结:使用CA 认证证书来保证‘服务器公钥’的正确性,使用非对称加密传递对称加密的密钥来保证对称加密的安全(数据传输的安全)。

抓包分析

tcp 三次握手

三次握手

ssl 握手

ssl 握手的过程主要分为两部分:获取服务端证书和传递随机生成的密钥用来后续加密通信。
ssl 握手
ssl 握手
ssl 握手
上面三个图是获取服务端证书的过程。流程如下:

  1. 客户端Client Hello
  2. 服务端Server Hello,Certificate,Server Key Exchange,Server Hello Done

总结:相当于是客户端通过Client Hello 告诉服务端即将进行ssl 通信,服务端验证能够进行ssl 通信,返回Server Hello 给客户端。然后将证书发送给客户端,最终发送Server Hello Done 代表第一个过程结束。这个过程的安全是通过CA,所以是安全的。

ssl 握手
上面是传递随机密钥的过程。流程如下:

  1. 客户端发送Client Key Exchange 来传递即将用来通信的密钥(该密钥被上面第一个步骤的服务器公钥加密,被称为Pre-master secret)。随后传递Change Cipher Spec 和Encrypted Handshake Message 来告诉服务器即将使用Pre-master secret 来加密。
  2. 服务端回复Change Cipher Spec 和Encrypted Handshake Message 可以使用Pre-master secret 加密来通信了。(这里的Session Ticket 不了解,但是不影响后面的分析)

总结: 通过服务端的公钥,安全的传递通信使用密钥。

通信

通信
通信
通信
最下面代表的是客户端发送给服务端的一个报文,数据是加密的。

tcp 四次分手

这里没有抓到报文就不展开了,一般需要在通信结束以后过一会儿才能抓到FIN 报文。

总结

整个过程没有明文传递任何客户端和服务端的数据,所以是安全的。

图解http中https 通信流程:
图解http中https 通信流程

app 如何避免被抓包

大部分app 的数据非常简单就被客户端Fiddler 或者charles。Fiddler 或者charles 代理的原理相当于是一个中间人的身份。客户端访问charles,把charles 当成服务端。charles 再访问最终服务器,服务器当charles 为客户端。
目前分析到的Android 端两种比较方便的方法(后面会写代码实现下):使用https 或者Android 端判断有没有连接代理。

Android 访问https 的方法

下面代码用来访问一个自签名的网站,主要是3个实现:trustNothing 无法访问Trust anchor for certification path not found.。trustAll 信任所有证书,可以访问,并且可以被抓包。trustOnly 信任唯一证书,无法被charles 抓包。还有一个实现isWifiProxy 可以用来判断有没有使用代理,控制是否请求数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
public class HttpsActivity extends AppCompatActivity {
private final static String TAG = "HttpsActivity";
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_https);
}
private void copyInputStreamToOutputStream(InputStream inputStream, PrintStream out)
throws IOException {
byte[] buffer = new byte[1024];
int c = 0;
while ((c = inputStream.read(buffer)) != -1) {
out.write(buffer, 0, c);
}
}
public void trustNothing(View view) {
// 使用默认情况,无法访问自签名网站
new Thread() {
@Override public void run() {
try {
//URL url = new URL("https://www.baidu.com/");
URL url = new URL("https://kyfw.12306.cn/otn/");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
public void trustAll(View view) {
// 方案1,信任所有的证书
new Thread() {
@Override public void run() {
try {
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[] {
new X509TrustManager() {
@Override public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}, null);
URL url = new URL("https://kyfw.12306.cn/otn/");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
SSLSocketFactory sslSocketFactory = context.getSocketFactory();
urlConnection.setSSLSocketFactory(sslSocketFactory);
urlConnection.setHostnameVerifier(new HostnameVerifier() {
@Override public boolean verify(String hostname, SSLSession session) {
return true;
}
});
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
}
}.start();
}
public void trustOnly(View view) {
// 方案2,信任单独的证书,使用Fiddler 抓包的话就看不到了
new Thread() {
@Override public void run() {
super.run();
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// srca.cer 打包在 asset 中
InputStream caInput = new BufferedInputStream(getAssets().open("srca.cer"));
Certificate ca;
try {
// 1. 获取到证书
ca = cf.generateCertificate(caInput);
((X509Certificate) ca).checkValidity();
Log.e(TAG, "ca=" + ((X509Certificate) ca).getSubjectDN());
Log.e(TAG, "key=" + ca.getPublicKey());
} finally {
caInput.close();
}
// Create a KeyStore containing our trusted CAs
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
// 2. 通过KeyStore 加载证书
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
URL url = new URL("https://kyfw.12306.cn/otn/");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
// 3. 设置SSL
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
InputStream inputStream = urlConnection.getInputStream();
copyInputStreamToOutputStream(inputStream, System.out);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
/**
* 判断有没有连接代理,这个可以用来控制如果连着代理就不访问服务器
*/
private boolean isWifiProxy() {
final boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
String proxyAddress;
int proxyPort;
if (IS_ICS_OR_LATER) {
proxyAddress = System.getProperty("http.proxyHost");
String portStr = System.getProperty("http.proxyPort");
proxyPort = Integer.parseInt((portStr != null ? portStr : "-1"));
} else {
proxyAddress = android.net.Proxy.getHost(this);
proxyPort = android.net.Proxy.getPort(this);
}
return (!TextUtils.isEmpty(proxyAddress)) && (proxyPort != -1);
}
}

相关工具

tPacketcapture.apk 以及wireshark

参考

Android Https相关完全解析 当OkHttp遇到Https